// Copyright (c) Microsoft Corporation. All rights reserved.

using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections;
using System.Runtime.InteropServices;

// TODO: Cache the various combinations of directory parts?

namespace Microsoft.Tools.CodeRunner
{
    /// <summary>
    /// Parsing typeHint for paths supplied to <see cref="ParsedPath"/> constructor.
    /// </summary>
    public enum PathType
    {
        /// <summary>
        /// The path is a file which may include a drive or share.
        /// </summary>
        File,
        /// <summary>
        /// The path is a directory which may include a drive or share.
        /// </summary>
        Directory,
        /// <summary>
        /// The path is just a root (drive or share)
        /// </summary>
        RootOnly,
        /// <summary>
        /// Try to automatically detect type of path.  Will assume path is a directory if 
        /// it has a trailing <see cref="Path.DirectorySeparatorChar"/> 
        /// </summary>
        Automatic,
    };

    /// <summary>
    /// A parses file system path.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Most of the time it is convenient to work with a file system path as a simple string.  However
    /// this can be a problem if the origin of the string is unknown, i.e. provided from user input, as 
    /// significant processing is required in order to ensure that a passed in string is syntactically
    /// correct.  Additionally it is often necessary to extract one or more components of the path for a 
    /// specific purpose.  This class makes both of these tasks significantly easier and more efficient 
    /// than working with the path as a string.  
    /// </para>  
    /// <para>Parsing a Windows file system path correctly is a tricky task. Paths can contain 
    /// Universal Naming Convention (UNC) components, they can contain relative directories, they
    /// might contain invalid characters, they can be quoted, they can contain wildcard characters, 
    /// they may refer to files, directories, or just a drive or UNC share. This class can handle all these 
    /// path components.</para>
    /// <para>This path also extends the normal Windows relative path syntax by allowing more than two 
    /// <code>.</code>'s to signify parents directories further up the directory tree.  This is perhaps syntactically
    /// neater that using the <code>..\..</code> syntax.</para>
    /// <para>
    /// The path undergoes the following pre-parsing fix-ups, checks and formatting: 
    /// <list typeHint="bullet">
    /// <item>A check is made that the path is not null</item>
    /// <item>The path is unquoted ("") if necessary</item>
    /// <item>Leading and trailing whitespace is trimmed, both before and after unquoting</item>
    /// <item>A check is made that the path is > 1 character long</item>
    /// <item>A check is made that the path does not contain any invalid path characters</item>
    /// <item>The <see cref="P:Directory"/> property is always given a terminating separator</item>
    /// <item><see cref="P:Drive"/> property will always have a drive letter separator character if it refers to a drive</item>
    /// <item>Extensions will always have a preceding <see cref="PathUtility.ExtensionSeparatorChar"/> character</item>
    /// <item>Trailing '.' and ' ' combinations are trimmed from all file and directory names</item>
    /// </list>
    /// </para>
    /// <para>
    /// This checking and formatting assures that properties representing concatenated sub-sections of the path, such as 
    /// <see cref="P:FileAndExtension"/> are always well formed. 
    /// </para>
    /// </remarks>
    [Serializable]
    public class ParsedPath : IComparable
    {
        #region Instance Data
        
        private string machine = String.Empty;
        private string share = String.Empty;
        private string drive = String.Empty;
        private string dir = String.Empty;
        private string fname = String.Empty;
        private string ext = String.Empty;

        #endregion

        #region Class Data
        
        private static readonly char[] BadDirTrailChars = new char[] {PathUtility.ExtensionSeparatorChar, ' ', '\t'};
        private static readonly char[] WildcardChars = new char[] {'*', '?'};
        
        /// <summary>
        /// Represents an empty, uninitialized <see cref="ParsedPath"/>
        /// </summary>
        public static readonly ParsedPath Empty = new ParsedPath();
        
        /// <summary>
        /// Returns a regular expression that searches for surrounding quotes for a string on a single line
        /// </summary>
        public static Regex SurroundingQuotesSingleLineRegex
        {
            get
            {
                if (surroundingQuotesSingleLineRegex == null)
                    surroundingQuotesSingleLineRegex = new Regex("\"([^\"]*)\"", RegexOptions.Singleline);
                
                return surroundingQuotesSingleLineRegex;
            }
        }
        
        private static Regex surroundingQuotesSingleLineRegex = null;
        
        /// <summary>
        /// Returns a regular expression that searches a relatative compononent of a path, e.g. .\, ..\, ...\ 
        /// </summary>
        public static Regex RelativePathPartSingleLineRegex
        {
            get
            {
                if (relativePathPartSingleLineRegex == null)
                    relativePathPartSingleLineRegex = new Regex(@"[.]+\\", RegexOptions.Singleline);
                
                return relativePathPartSingleLineRegex;
            }
        }
        
        private static Regex relativePathPartSingleLineRegex = null;
        
        #endregion

        #region Instance Constructors
        // For internal use
        private ParsedPath()
        {
        }

        /// <summary>
        /// Initializes an instance of the <see cref="T:ParsedPath"/> class by parsing the given file 
        /// system path into its constituent parts, assuming it includes a file name.
        /// </summary>
        /// <exception cref="T:ArgumentException">The path could not be parsed.  The exception contains
        /// a description of the problem.</exception>
        /// <param name="path">The file path to parse.</param>
        public ParsedPath(string path) : this(path, PathType.Automatic) { }
        
        /// <summary>
        /// Initializes an instance of the <see cref="T:ParsedPath"/> class by parsing the given file 
        /// system path into its constituent parts.
        /// </summary>
        /// <exception cref="T:ArgumentException">The path could not be parsed.  The exception contains
        /// a description of the problem.</exception>
        /// <param name="path">The file path to parse.</param>
        /// <param name="typeHint">See <see cref="T:PathType"/> for a list of path type hints</param>
        public ParsedPath(string path, PathType typeHint)
        {
            // Null reference is bad
            if (path == null)
                throw new ArgumentNullException("path");

            // Remove leading/trailing spaces 
            path = path.Trim();
            
            // Remove any surrounding quotes
            if (path.Length >= 2 && path[0] == '\"')
            {
                path = SurroundingQuotesSingleLineRegex.Replace(path, @"$1");
                path = path.Trim(null);
            }

            // Do we still have anything?
            if (path.Length == 0)
                throw new ArgumentException("Path is zero length");
                
            // Do an invalid character check once now
            if (path.IndexOfAny(Path.GetInvalidPathChars()) != -1)
                throw new ArgumentException("Path contains invalid characters");

            // Convert '/' into '\'
            path = path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);

            int i; // Always the beginning index
            int j; // Always the ending index

            bool isUnc = false;
            bool autoTypeHint = (typeHint == PathType.Automatic);

            if (path.StartsWith(PathUtility.UncPrefixChars, StringComparison.CurrentCultureIgnoreCase))
            {
                i = 0;
                
                if (i + PathUtility.UncPrefixChars.Length >= path.Length)
                    throw new ArgumentException("Badly formed UNC name");
                    
                // Find the '\' after the '\\'
                j = path.IndexOf(Path.DirectorySeparatorChar, PathUtility.UncPrefixChars.Length);			
                
                if (j == -1 || j - i == 0)
                    throw new ArgumentException("Badly formed UNC name");
                    
                machine = path.Substring(0, j);

                i = j;
                
                // Find the '\' after the share name, if there is one and it's not bang up
                // against the next one
                if (i + 1 >= path.Length || path[i + 1] == Path.DirectorySeparatorChar)
                    throw new ArgumentException("Badly formed UNC name");
                
                j = path.IndexOf(Path.DirectorySeparatorChar, i + 1);

                // Either it wasn't found or the string ended
                if (j == -1)
                    j = path.Length;
                
                if (j - i == 0)
                    throw new ArgumentException("Badly formed UNC name");

                share = path.Substring(i, j - i);

                if (typeHint == PathType.Automatic && path.Length == Root.Length)
                    typeHint = PathType.RootOnly;
                    
                isUnc = true;
            }
            else if (path.Length >= 2 && path[1] == Path.VolumeSeparatorChar)
            {
                drive = path.Substring(0, 2);
                
                if (typeHint == PathType.Automatic && path.Length == 2)
                    typeHint = PathType.RootOnly;
            }

            if (typeHint == PathType.Automatic)
            {
                if (path[path.Length - 1] == Path.DirectorySeparatorChar)
                {
                    typeHint = PathType.Directory;
                }
                else
                {			
                    typeHint = PathType.File;
                }
            }
            
            if (typeHint == PathType.File)	
            {
                // Get the file name
                i = path.LastIndexOfAny(
                    new char[2] {Path.DirectorySeparatorChar, Path.VolumeSeparatorChar}, path.Length - 1);
                
                // If we didn't find anything we must have an unrooted file
                if (i == -1)
                {
                    i = 0;
                }
                else 	
                {
                    if (i + 1 >= path.Length)
                        throw new ArgumentException("Path is badly formed");

                    // Move past the separator; that's part of the directory
                    i++;
                }

                // There is a file name so we can continue.  Now that we know that, we can trim the end of 
                // the path.
                path = path.TrimEnd(BadDirTrailChars);
                
                // Chop off the file name
                fname = path.Substring(i, path.Length - i);

                j = fname.LastIndexOf(PathUtility.ExtensionSeparatorChar);

                if (j != -1 && j + 1 < fname.Length)
                {
                    // Chop off the extension which is everything from the last '.' in the file name (if any)
                    ext = fname.Substring(j);
                    fname = fname.Substring(0, j);
                }
            }
                
            // Get the directory - it's everything that's left (which may be nothing)
            if (isUnc)
                dir = path.Substring(machine.Length + share.Length, 
                    path.Length - machine.Length - share.Length - fname.Length - ext.Length);
            else
                dir = path.Substring(drive.Length, 
                    path.Length - drive.Length - fname.Length - ext.Length);

            // You have got to have at least a directory
            if (typeHint == PathType.Directory && dir == String.Empty)
                throw new ArgumentException("Missing directory");

            // You can't have wildcards in the directory part or double '\\' characters
            if (dir.IndexOfAny(WildcardChars) != -1 ||
                dir.IndexOf(PathUtility.UncPrefixChars, StringComparison.CurrentCultureIgnoreCase) != -1)
                throw new ArgumentException("Invalid characters in path");
                
            // If user wanted a RootOnly, validate that we have no directory, file or extension
            if (!autoTypeHint && typeHint == PathType.RootOnly && 
                (dir != String.Empty || fname != String.Empty || ext != String.Empty))
                throw new ArgumentException("Path contains more than just a root");

            // Now we go through the directory and remove trailing space/dot combinations
            string[] dirParts = dir.Split(new char[] {Path.DirectorySeparatorChar});
            StringBuilder sb = new StringBuilder(dir.Length);
            
            for (i = 0; i < dirParts.Length; i++)
            {
                string dirPart = dirParts[i];
            
                // An empty dirPart is either a leading or trailing '\'
                if (dirPart == "")
                {
                    // Don't add the trailing on back in as it's already been done in the last iteration
                    if (i != dirParts.Length - 1)
                        sb.Append(Path.DirectorySeparatorChar);
                    
                    continue;
                }
                
                // Remove trailing spaces
                dirPart = dirPart.TrimEnd();

                // Deals with '\   \' situation
                if (dirPart == "")
                    throw new ArgumentException("Empty directory name");
                    
                // Only trim remaining dots and spaces if the directory is not all dots (relative)  
                for (j = dirPart.Length - 1; j >= 0; j--)
                    if (dirPart[j] != PathUtility.ExtensionSeparatorChar)
                        break;
                
                if (j > 0)
                {
                    dirPart = dirPart.TrimEnd(BadDirTrailChars);
                    
                    // Deals with the '\.. . . ..   .....\' situation
                    if (dirPart == "")
                        throw new ArgumentException("Empty directory name");
                }
                
                sb.Append(dirPart);
                sb.Append(Path.DirectorySeparatorChar);
            }
            
            dir = sb.ToString();
        }

        #endregion

        #region Instance Methods
        /// <summary>
        /// Parse a string and return a <see cref="ParsedPath"/> object.
        /// </summary>
        /// <param name="value">String value to parse.</param>
        /// <returns></returns>
        public ParsedPath Parse(string value)
        {
            return new ParsedPath(value);
        }

        /// <summary>
        /// Combine a path fragment with an existing path. You can only combine to the path if it is not 
        /// contain a filename or extension.  The path fragment being combined must not contain a root.
        /// </summary>
        /// <param name="path">A path that is a directory fragment and/or a filename/extension</param>
        /// <returns></returns>
        public ParsedPath Combine(string path)
        {
            return Combine(path, PathType.Automatic);
        }

        /// <summary>
        /// Combine a path fragment with an existing path. You can only combine to the path if it is not 
        /// contain a filename or extension.
        /// </summary>
        /// <param name="path">A path that is a directory fragment and/or a filename</param>
        /// <param name="pathType">The type of the passed in path fragment</param>
        /// <returns></returns>
        public ParsedPath Combine(string path, PathType pathType)
        {
            if (this.IsFilename)
                throw new ArgumentException("Target path already has a file name and/or extension");
         
            ParsedPath pp = new ParsedPath(path, pathType);

            if (pp.HasRoot)
                throw new ArgumentException("Path fragment must not contain a root");

            ParsedPath pp2 = new ParsedPath();

            pp2.share = this.share;
            pp2.drive = this.drive;
            pp2.dir = this.dir + pp.dir;
                
            if (pp.HasFilename)
            {
                pp2.fname = pp.fname;
                pp2.ext = pp.ext;
            }
            
            return pp2;
        }        
        
        /// <overloads>Creates a fully qualified file path.</overloads>
        /// <summary>
        /// Creates a fully qualified file path from a partial path and the current directory.
        /// </summary>
        /// <remarks>
        /// This method is equivalent to <see cref="T:MakeFullPath(System.String)"/> 
        /// with the applications current directory as the base path.
        /// </remarks>
        /// <returns>The fully qualified file path</returns>
        public ParsedPath MakeFullPath()
        {
            return MakeFullPath(null);
        }
        

        /// <summary>
        /// Makes the <see cref="ParsedPath"/> fully qualified using the specified base path. 
        /// </summary>
        /// <remarks>
        /// <para>
        /// The <see cref="ParsedPath"/> may have a missing root and contain relative directories.  A path root is a drive letter or machine/share 
        /// and root directories.  The method returns a path containing both a full root and no relative directories.  If the <see cref="ParsedPath"/>  
        /// does not contain a drive or machine/share the function begins by prefixing the <see cref="ParsedPath"/> with the base path. 
        /// </para>
        /// <para>
        /// In addition to the standard '.' and '..' relative directories, you can specify three or more periods to represent
        /// increasing levels of parent directory. 
        /// </para>
        /// <para>The function does not check to see if the resulting path points to valid file or directory.</para> 
        /// </remarks>
        /// <param name="basePath">The base path to use.  It must have a root and directory parts.  Any file or extension is ignored.</param>
        /// <exception cref="System.ArgumentException">
        /// The path contains too many parent directories, such that the path would traverse beyond the root drive or machine/share, 
        /// or the path contains invalid file name characters.
        /// </exception>
        /// <returns>The fully qualified file system path.</returns>
        public ParsedPath MakeFullPath(ParsedPath basePath)
        {
            if (basePath == null)
                basePath = new ParsedPath(System.Environment.CurrentDirectory, PathType.Directory);
            
            if (!basePath.HasRoot)
                throw new ArgumentException("Base directory has no root");
                
            if (basePath.Directory == String.Empty)
                throw new ArgumentException("Base directory has no directory");
        
            ParsedPath ppNew = new ParsedPath();
        
            // Copy the file and extension
            ppNew.fname = fname;
            ppNew.ext = ext;
            
            // Does this path contain a root?
            if (HasRoot)
            {
                // Yes, copy this paths root
                if (HasUncRoot)
                {
                    ppNew.machine = machine;
                    ppNew.share = share;	
                }
                else
                {
                    ppNew.drive = drive;
                }
            }
            else
            {
                // No, copy base path root
                if (basePath.HasUncRoot)
                {
                    ppNew.machine = basePath.machine;
                    ppNew.share = basePath.share;	
                }
                else
                {
                    ppNew.drive = basePath.drive;
                }
            }

            StringBuilder sb = new StringBuilder(dir.Length);
            int index = 0;  
            // This is the index of the first character of the remainder of the unprocess part of the directory

            // Does this path contain a rooted directory, i.e. does it start with '\'?
            if (dir.Length > 0 && dir[index] == Path.DirectorySeparatorChar)
            {	
                // Yes, start with the root directory
                sb.Append(dir[index++]);
            }
            else
            {
                // No, start the new directory with the base path directory
                sb.Append(basePath.Directory);
            }

            // Find the offset of any relative part of this path
            Match m = RelativePathPartSingleLineRegex.Match(dir);
                
            while (m.Success)
            {
                int n = m.Index;

                // Move past the non-relative portion
                sb.Append(dir, index, m.Index - index);
                index = m.Index;
            
                // Count the dots
                int dotCount = 0;
                
                for (; dir[index] == PathUtility.ExtensionSeparatorChar; index++, dotCount++);
                
                // Jump over the '\'
                index++;
                
                // Only dot counts greater than one have any effect
                dotCount--;
                
                for (; dotCount > 0; dotCount--)
                {
                    // Find the start of the last directory
                    int i = sb.Length - 2;
                    
                    for (; i >= 0 && sb[i] != Path.DirectorySeparatorChar; i--);
                    
                    if (i < 0)
                        throw new ArgumentException("To many relative directories in path");

                    sb.Remove(i + 1, sb.Length - i - 1);
                }
                
                m = m.NextMatch();
            }
        
            // Add any trailing non-relative part
            sb.Append(dir, index, dir.Length - index);
            
            ppNew.dir = sb.ToString();
        
            return ppNew;
        }

    
        /// <summary>
        /// Makes a new path which is this path made relative to a base path.  The <see cref="ParsedPath"/> can 
        /// only be made relative if <see cref="IsFull"/> returns <code>true</code> for this path and the base path,
        /// and the <see cref="ParsedPath.Root"/> must be the same for both paths.
        /// Only the directory portion of the new path will differ from the original path.
        /// </summary>
        /// <param name="basePath">The new path will be relative to the directory of this path</param>
        /// <returns>This path made relative to the give base path</returns>
        public ParsedPath MakeRelativePath(ParsedPath basePath)
        {
            if (!this.IsFull && !basePath.IsFull)
                throw new ArgumentException("Both paths must be fully qualified");
                
            if (this.Root != basePath.Root)
                throw new ArgumentException("Both paths must have the same root (drive or UNC name)");

            ParsedPath ppNew = new ParsedPath();
            
            ppNew.fname = this.fname;
            ppNew.ext = this.ext;
            ppNew.drive = this.drive;
            ppNew.share = this.share;
            
            // Get the directory portions of both paths as arrays
            string[] dirs = this.SubDirectories;
            string[] baseDirs = basePath.SubDirectories;
            int min = Math.Min(dirs.Length, baseDirs.Length);
            
            // Skip over any directory names that are the same
            int n = 0;
            
            while (n < min)
            {
                if (String.Compare(dirs[n], baseDirs[n], true) != 0)
                    break;
                    
                n++;
            }

            StringBuilder sb = new StringBuilder(this.Directory.Length);

            // If there is nothing left of the base directory, add a '.' then the rest of the directories in this path
            if (baseDirs.Length - n == 0)
            {
                sb.Append(PathUtility.ExtensionSeparatorChar);
                sb.Append(Path.DirectorySeparatorChar);
                
                for (; n < dirs.Length; n++)
                {
                    sb.Append(dirs[n]);
                    sb.Append(Path.DirectorySeparatorChar);
                }
            }
            else
            {
                // Otherwise add ..'s for number of remaining directories in base then the rest of the directories in this path
                int m = n;
                
                for (; n < baseDirs.Length; n++)
                {
                    sb.Append(PathUtility.ExtensionSeparatorChar);
                    sb.Append(PathUtility.ExtensionSeparatorChar);
                    sb.Append(Path.DirectorySeparatorChar);
                }

                for (n = m; n < dirs.Length; n++)
                {
                    sb.Append(dirs[n]);
                    sb.Append(Path.DirectorySeparatorChar);
                }
            }
            
            ppNew.dir = sb.ToString();
            
            return ppNew;
        }


        /// <summary>
        /// Makes a path that corresponds the immediate parent directory of the existing path.   <see cref="ParsedPath.MakeFullPath()"/> is 
        /// called before the parent is determined, using the current directory as the base path,
        /// in case the given path is relative.
        /// </summary>
        /// <returns>A <see cref="ParsedPath"/> object with the immediate parent directory of the original directory</returns>
        public ParsedPath MakeParentPath()
        {
            return MakeParentPath(-1, null);
        }

        /// <summary>
        /// Makes a path that corresponds the a parent directory of the existing path.   <see cref="ParsedPath.MakeFullPath()"/> is 
        /// called before the parent is determined, using the current directory as the base path,
        /// in case the given path is relative.
        /// </summary>
        /// <param name="depthChange">The relative depth level of the parent.  -1 is the immediate parent, then -2 and so on.</param>
        /// <exception cref="ArgumentOutOfRangeException">If the number of levels specified is more than the number of levels in the path, i.e. would go below the 
        /// root directory of the path.</exception>
        /// <returns>A <see cref="ParsedPath"/> object with the specified parent directory of the original directory</returns>
        public ParsedPath MakeParentPath(int depthChange)
        {
            return MakeParentPath(depthChange, null);
        }

        /// <summary>
        /// Makes a path that corresponds the immediate parent directory of the existing path.   <see cref="ParsedPath.MakeFullPath()"/> is 
        /// called before the parent is determined, using the current directory as the base path, in case the given path is relative.
        /// </summary>
        /// <param name="basePath">Base path to use when fully qualifying a relative path</param>
        /// <exception cref="ArgumentOutOfRangeException">If the number of levels specified is more than the number of levels in the path, i.e. would go below the 
        /// root directory of the path.</exception>
        /// <returns>A <see cref="ParsedPath"/> object with the specified parent directory of the original directory</returns>
        public ParsedPath MakeParentPath(ParsedPath basePath)
        {
            return MakeParentPath(-1, basePath);
        }

        /// <summary>
        /// Makes a path that corresponds the immediate parent directory of the existing path.   <see cref="ParsedPath.MakeFullPath()"/> is 
        /// called before the parent is determined using <code>basePath</code> as the base directory.
        /// </summary>
        /// <param name="depthChange">The depth level of the parent.  -1 is the immediate parent, the next -2 and so on.</param>
        /// <param name="basePath">Base path to use when fully qualifying a relative path</param>
        /// <exception cref="ArgumentOutOfRangeException">If the number of levels specified is more than the number of levels in the path, i.e. would go below the </exception>
        /// <returns><code>null</code> if this method is called on the root directory, otherwise a <see cref="ParsedPath"/>
        /// containing same root, file and extension and a directory equivalent to the parent of the old directory.</returns>
        public ParsedPath MakeParentPath(int depthChange, ParsedPath basePath)
        {
            if (IsRootDirectory)
                throw new ArgumentException("Path is already root directory");
                
            if (depthChange >= 0)
                throw new ArgumentOutOfRangeException("depthChange", "Depth change must be < 0");
            
            ParsedPath ppNew = new ParsedPath();
            ParsedPath ppFull = this.MakeFullPath(basePath);
            
            // Copy the stuff that is unchanged
            ppNew.drive = ppFull.drive;
            ppNew.share = ppFull.share;
            ppNew.machine = ppFull.machine;
            ppNew.fname = ppFull.fname;
            ppNew.ext = ppFull.ext;
            
            // Find the nth parent
            int i = ppFull.dir.Length;
            
            while (depthChange++ < 0)
            {
                if (i == 1)
                    throw new ArgumentOutOfRangeException("depthChange", "Number of levels specified would go beyond root directory.");
                    
                i = ppFull.dir.LastIndexOf(Path.DirectorySeparatorChar, i - 2) + 1;
            }			
            
            // Extract the parent directory
            ppNew.dir = ppFull.dir.Substring(0, i);
            
            return ppNew;
        }
        
        #endregion

        #region Overrides
        /// <summary>
        /// Returns a <see cref="T:System.String"/> equivalent to the entire path 
        /// </summary>
        public override string ToString()
        {
            return this.All;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            ParsedPath pp = obj as ParsedPath;
            
            if (pp == null)
                return false;

            return				
                this.machine == pp.machine &&
                this.share == pp.share &&
                this.drive == pp.drive &&
                this.dir == pp.dir &&
                this.fname == pp.fname &&
                this.ext == pp.ext;
        }


        /// <summary>
        /// Returns <code>true</code> if two <see cref="ParsedPath"/> objects are equal.
        /// </summary>
        /// <param name="pp1"></param>
        /// <param name="pp2"></param>
        /// <returns></returns>
        public static bool Equals(ParsedPath pp1, ParsedPath pp2)
        {
            if ((object)pp1 == (object)pp2)
                return true;
                
            if ((object)pp1 == null || (object)pp2 == null)
                return false;
                
            return pp1.Equals(pp2);
        }

        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            return this.ToString().GetHashCode();
        }

        /// <summary>
        /// Compares two parsed paths for equivalence.
        /// </summary>
        /// <remarks>This method uses <see cref="String.Compare(string, string, bool)"/> to compare the two paths</remarks>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <param name="ignoreCase"></param>
        /// <returns></returns>
        public static int Compare(ParsedPath a, ParsedPath b, bool ignoreCase)
        {
            return String.Compare(a.ToString(), b.ToString(), 
                ignoreCase ? StringComparison.CurrentCultureIgnoreCase : StringComparison.CurrentCulture);
        }

        /// <summary>
        /// Compares two parsed paths for equivalence.
        /// </summary>
        /// <remarks>This method uses <see cref="String.Compare(string, string)"/> to compare the two objects</remarks>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public static int Compare(ParsedPath a, ParsedPath b)
        {
            return Compare(a, b, true);
        }
        
        /// <summary>
        /// Compare this to another for equivalence.
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public int CompareTo(object obj)
        {
            if (obj == null)
                return 1;			
        
            ParsedPath path = obj as ParsedPath;
            
            if (path == null) 
                throw new ArgumentException("Object is not a ParsedPath");

            return String.Compare(this.ToString(), path.ToString(), StringComparison.CurrentCultureIgnoreCase);
        }

        #endregion

        #region Operators and Conversions
        /// <summary>
        /// Implicit conversion to a string
        /// </summary>
        /// <param name="pp">The parsed path to convert</param>
        /// <returns>A string containing equivalent to the entire path</returns>
        public static implicit operator String(ParsedPath pp)
        {
            if (pp == null)
                return null;
                
            return pp.ToString();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="pp1"></param>
        /// <param name="pp2"></param>
        /// <returns></returns>
        public static bool operator ==(ParsedPath pp1, ParsedPath pp2)
        {
            return Equals(pp1, pp2);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="pp1"></param>
        /// <param name="pp2"></param>
        /// <returns></returns>
        public static bool operator !=(ParsedPath pp1, ParsedPath pp2)
        {
            return !Equals(pp1, pp2);
        }

        #endregion

        #region Instance Properties
        /// <summary>
        /// Gets a boolean value which indicates whether the path has UNC root
        /// </summary>
        public bool HasUncRoot
        {
            get 
            {
                // If machine is set, share must be by definition
                return (machine != String.Empty);
            }
        }
            
        /// <summary>
        /// Gets a boolean value which indicates whether the path has drive root
        /// </summary>
        public bool HasDriveRoot
        {
            get 
            {
                // If machine is set, share must be by definition
                return (drive != String.Empty);
            }
        }

        /// <summary>
        /// Gets a boolean value indicating whether the path has a directory
        /// </summary>
        public bool HasDirectory
        {
            get
            {
                return (dir != String.Empty);
            }
        }

        /// <summary>
        /// Gets a boolean value indicating whether the path has a file name
        /// </summary>
        public bool HasFilename
        {
            get
            {
                return (fname != String.Empty);
            }
        }

        /// <summary>
        /// Gets a boolean value indicating whether the path has an extension
        /// </summary>
        public bool HasExtension
        {
            get
            {
                return (ext != String.Empty);
            }
        }

        /// <summary>
        /// Gets a boolean value which indicates the path file name contains wildcard characters '*' or '?'.
        /// </summary>
        public bool HasWildcards
        {
            get
            {
                return (FileAndExtension.IndexOfAny(WildcardChars) != -1);
            }
        }

        /// <summary>
        /// Gets a boolean value which indicates whether the path has a root (drive or machine/share)
        /// </summary>
        public bool HasRoot
        {
            get 
            {
                return HasDriveRoot || HasUncRoot;
            }
        }
            
        /// <summary>
        /// Gets a boolean value which indicates whether the directory part refers to the root directory.
        /// <code>false</code> is returned if there is no directory part.
        /// </summary>
        public bool IsRootDirectory
        {
            get 
            {
                return (dir.Length == 1 && dir[0] == Path.DirectorySeparatorChar);
            }
        }

        /// <summary>
        /// Gets a boolean value which indicates if the path is a only a root, i.e. it contains no directory, filename
        /// or extension.
        /// </summary>
        public bool IsRootOnly
        {
            get
            {
                return HasRoot && !HasDirectory && !HasFilename && !HasExtension;
            }
        }

        /// <summary>
        /// Gets a boolean value which indicates if the path is a directory, i.e. it contains no filename and no extension.
        /// </summary>
        public bool IsDirectory
        {
            get
            {
                return (!HasFilename && !HasExtension);
            }
        }

        /// <summary>
        /// Gets a boolean value which indicates if the path is a directory only, i.e. it contains no root, filename
        /// or extension.
        /// </summary>
        public bool IsDirectoryOnly
        {
            get 
            {
                return (!HasRoot && !HasFilename && !HasExtension);
            }
        }

        /// <summary>
        /// Gets a boolean value which indicates if the path is a filename, i.e. it contains a filename and/or extension
        /// </summary>
        public bool IsFilename
        {
            get
            {
                return (HasFilename || HasExtension);
            }
        }

        /// <summary>
        /// Gets a boolean value which indicates if the path is a filename only, i.e. it contains no root or directory.
        /// </summary>
        public bool IsFilenameOnly
        {
            get
            {
                return (!HasRoot && !HasDirectory);
            }
        }

        /// <summary>
        /// Gets a boolean value which indicates if the path contains a relative directory.
        /// </summary>
        public bool IsRelative
        {
            get 
            {
                Match m = RelativePathPartSingleLineRegex.Match(dir);
                
                return m.Success;
            }
        }
            
        /// <summary>
        /// Gets a boolean value which indicates if the path is fully qualified.
        /// </summary>
        public bool IsFull
        {
            get 
            {
                return HasRoot && !IsRelative;
            }
        }
        
        /// <summary>
        /// Gets the depth (the number of sub-directories) of the directory part of a path, starting from 0 for the root directory.  Throws
        /// <exception cref="System.ArgumentException">Thrown when the path has relative directories</exception>
        /// </summary>
        public int DirectoryDepth
        {
            get
            {
                if (IsRelative)
                    throw new ArgumentException("Path has relative directories");
                
                int count = 0;
                string dir = Directory;
                
                for (int i = 0; i < dir.Length; i++)
                    if (dir[i] == Path.DirectorySeparatorChar)
                        count++;
                        
                return count;
            }
        }
        
        /// <summary>
        /// Gets the <see cref="Directory"/> part split into its constituent sub-directories. Does not fail if 
        /// the directory contains relative parts.
        /// </summary>
        public string[] SubDirectories
        {
            get 
            {
                string[] subDirs = DirectoryNoSeparator.Split(new char[] {Path.DirectorySeparatorChar});
                
                // Fix the root directory
                subDirs[0] = Path.DirectorySeparatorChar.ToString();
                
                return subDirs;
            }
        }
        
        /// <summary>
        /// Get the machine name.
        /// </summary>
        public string Machine { get { return machine; } }
        /// <summary>
        /// Get the share name.
        /// </summary>
        public string Share { get { return share; } }
        /// <summary>
        /// Get the drive name.  
        /// </summary>
        public string Drive { get { return drive; } }
        /// <summary>
        /// Get the concatenated machine/share values.
        /// </summary>
        public string MachineAndShare { get { return machine + share; } }
        /// <summary>
        /// Get the path root which is one of either machine/share or drive.  
        /// </summary>
        public string Root 
        { 
            get 
            { 
                if (drive != String.Empty)
                    return drive; 
                else
                    return machine + share;
            } 
        }
        /// <summary>
        ///	Get the directory part of the path.  
        /// </summary>
        public string Directory { get { return dir; } }
        /// <summary>
        ///	Get the name of the last folder in the directory name, with no trailing separator
        /// </summary>
        public string DirectoryLastFolderNoSeparator
        { 
            get 
            { 
                string folder = String.Empty;
            
                if (!IsRootDirectory)
                {
                    folder = DirectoryNoSeparator;
                    
                    folder = folder.Substring(folder.LastIndexOf(Path.DirectorySeparatorChar) + 1); 
                }
                
                return folder;
            } 
        }
        /// <summary>
        ///	Get the directory part of the path.  
        /// </summary>
        public string DirectoryNoSeparator 
        { 
            get 
            { 
                return dir.TrimEnd(new char[] {Path.DirectorySeparatorChar}); 
            } 
        }
        /// <summary>
        /// Get the file name part of the path.
        /// </summary>
        public string File { get { return fname; } }
        /// <summary>
        /// Get the extension part of the path.  
        /// </summary>
        public string Extension { get { return ext; } }
        /// <summary>
        /// Get the concatenated drive and directory.
        /// </summary>
        public string RootAndDirectory { get { return Root + dir; } }
        /// <summary>
        /// Get the concatenated drive and directory.
        /// </summary>
        public string RootAndDirectoryNoSeparator 
        { 
            get 
            { 
                return (Root + dir).TrimEnd(new char[] {Path.DirectorySeparatorChar}); 
            } 
        }
        /// <summary>
        /// Get the concatenated directory and file.
        /// </summary>
        public string DirectoryAndFile { get { return dir + fname; } }
        /// <summary>
        /// Get the concatenated file name and extension.
        /// </summary>
        public string FileAndExtension { get { return fname + ext; } }
        /// <summary>
        /// Get the concatenated machine, share, directory and file.
        /// </summary>
        public string RootDirectoryAndFile { get { return Root + dir + fname; } }
        /// <summary>
        /// Get the concatenated directory, file and extension.
        /// </summary>
        public string DirectoryFileAndExtension { get { return dir + fname + ext; } }
        /// <summary>
        /// Get all parts of the path concatenated together (the full path).
        /// </summary>
        public string All { get { return Root + dir + fname + ext; } }
        #endregion
    }
}
